/*
 * Copyright (C) 2017 Gautam Chibde
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.chibde.visualizer;

import com.chibde.BaseVisualizer;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.app.Context;

import java.util.HashMap;
import java.util.Map;

/**
 * Custom component that creates a Circle and Bar visualizer effect for the ohos
 * <p>
 * Created by gautam chibde on 20/11/17. Smooth effect added by Ali heidari
 */
public class CircleBarVisualizerSmooth extends BaseVisualizer implements Component.DrawTask {
    private static final float _StepsCount = 2;
    private static final int _BarCount = 120;
    private static final float _AngleStep = 360f / _BarCount;
    private float[] points;
    private float[] endPoints;
    private float[] diffs;

    // Stores radius and step-counter which every invoking of "onDraw" requires them
    private Map<String, Integer> configs = null;

    /**
     * constructor
     *
     * @param context context
     * @param attrs attrs
     */
    public CircleBarVisualizerSmooth(Context context, AttrSet attrs) {
        super(context, attrs);
        addDrawTask(this);
    }

    @Override
    protected void init() {
        paint.setStyle(Paint.Style.STROKE_STYLE);
    }

    /*
     * Returns the value of given configuration-key with handling
     *
     * @see java.lang#NullPointerException
     */
    private int getConfig(String key) {
        Object obj = configs.get(key);
        if (obj != null) {
            return (int) obj;
        } else {
            return 0;
        }
    }

    /*
     * set new value of given configuration-key
     */
    private void setConfig(String key, int value) {
        configs.put(key, value);
    }

    /*
     * Get smaller dimension of visualizer
     */
    private int getSmallerDimen() {
        if (getHeight() < getWidth()) {
            return getHeight();
        } else {
            return getWidth();
        }
    }

    /*
     * Fill the initial configurations
     */
    private void fillConfigs() {
        if (configs != null) {
            return;
        }
        configs = new HashMap<>();

        // Calculates the radius of center circle.
        // Formula disclaimer : 0.65 = 3.14 * 0.02
        int radius = (int) (getSmallerDimen() * 0.65 / 2) * 6 / 10;

        // Width of each bar
        double circumference = 1.5 * Math.PI * radius;
        paint.setStrokeWidth((float) (circumference / _BarCount));

        // 0 = false, 1 = true
        configs.put("needsInit", 1);
        configs.put("radius", radius);
        configs.put("stepCounter", 0);
    }

    /*
     * Initializes the points
     */
    private void initPoints() {
        // Set points sizes if it is first time we got here or for any reasons arrays
        // are broken.
        if (getConfig("needsInit") == 1 || points == null || points.length < bytes.length * 2) {
            // It needs to multiply by 4 because for every byte should be
            // StartX,StartY,EndX,EndY
            points = new float[bytes.length * 4];

            // It needs to multiply by 4 because for every byte should be EndX,EndY,OldEndX,OldEndY
            endPoints = new float[bytes.length * 4];

            // It needs to multiply by 2 because there are X and Y differences
            diffs = new float[bytes.length * 2];
        }
    }

    /*
     * Fill the points for end of each bar.
     * Only needs to calculate the end of bar-line, because starting is not changing
     */
    private void fillPoints(int round, int i) {
        int indexM2 = i * 2;
        int indexM4 = i * 4;

        // Increase/Decrease the length of bar so oldEnd can match with ends
        if (round <= _StepsCount) {
            // Find endX to be drawn
            points[indexM4 + 2] = endPoints[indexM4 + 2] + diffs[indexM2] * round;

            // Find endX to be drawn
            points[indexM4 + 3] = endPoints[indexM4 + 3] + diffs[indexM2 + 1] * round;
        }
    }

    /*
     * Fills the end points and differences
     */
    private void fillEndPointsAndDiffs(int i, float newX, float newY) {
        // Set the old ends before assign new value the ends
        endPoints[i * 4 + 2] = endPoints[i * 4];
        endPoints[i * 4 + 3] = endPoints[i * 4 + 1];

        // Find endX
        endPoints[i * 4] = newX;

        // Find endY
        endPoints[i * 4 + 1] = newY;

        // If it is not first time, so we have oldEnds for calculation of differences
        if (getConfig("needsInit") == 0) {
            // Find differences of Xs
            diffs[i * 2] = (endPoints[i * 4] - endPoints[i * 4 + 2]) / _StepsCount;

            // Find differences of Ys
            diffs[i * 2 + 1] = (endPoints[i * 4 + 1] - endPoints[i * 4 + 3]) / _StepsCount;
        } else {
            // Set the old ends
            endPoints[i * 4 + 2] = endPoints[i * 4];
            endPoints[i * 4 + 3] = endPoints[i * 4 + 1];
        }
    }

    /*
     * Calculates the legth of bar
     */
    private int getBarLength(int i, float ceiling) {
        // Find the index of byte inside buffer
        int x = (int) Math.ceil(i * ceiling);

        // Change the sign of byte
        byte a = (byte) (-Math.abs(bytes[x]) + 128);

        // Gets the length of the line
        return a * (getHeight() / 4) / 128;
    }

    /*
     * Calculate first points
     */
    private void fillStartingPoints(int i, double angle) {
        int indexM4 = i * 4;

        // First time calculates the startX and startY for every byte
        if (getConfig("needsInit") == 1) {
            // Find startX
            points[indexM4] = (float) (this.getWidth() / 2 + getConfig("radius") * Math.cos(angle));

            // Find startY
            points[indexM4 + 1] = (float) (this.getHeight() / 2 + getConfig("radius") * Math.sin(angle));
        }
    }

    /*
     * Draw waveform It calculates the StartX,StartY just once because it never
     * changes. Then calculates EndX, EndY, OldEndX and OldEndY every 3 frames. So
     * OldEndX and OldEndY can increase/decrease toward EndX and EndY respectively.
     * To perform such an action(Animation) you need differences of X and Y. It
     * achieves using EndX - OldEndX and EndY - OldEndY Then find the steps using
     * Differences / 3 Finally when OldEnd(s) matched to End(s) Need to set End with
     * OldEnd value And the action will be repeated until visualizer is running.
     */
    /*
     * Reset configs
     */
    private void resetConfigs() {
        // The stepCounter increases
        setConfig("stepCounter", getConfig("stepCounter") + 1);

        // Initialized, no longer need initializing
        if (getConfig("needsInit") == 1) {
            setConfig("needsInit", 0);
        }
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        // Check if bytes initiated before
        if (bytes == null) {
            return;
        }

        // Init configs
        fillConfigs();

        // Fill the points
        initPoints();

        // We start with angle 0 and go against clock's direction
        double angle = 0;

        // Calculates every points and iterate along increasing angle
        for (int i = 0; i < _BarCount; i++, angle += _AngleStep) {
            // Convert to radians
            double radianAngle = Math.toRadians(angle);
            this.fillStartingPoints(i, radianAngle);
        }
        if (getConfig("needsInit") == 0) {
            canvas.drawLines(points, paint);
        }

        // Resets configurations variable for next calling of onDraw
        this.resetConfigs();
    }
}